<?php

class SlidingWindow
{
    // 限制单位时间内最多跑多少次
    const LIMIT_COUNT = 100;
    // 限制单位时间内最多跑多少次禁止数量
    const LIMIT_BAN_COUNT = 300;


    // 多少时间间隔
    const LIMIT_UNIT = 60;


    // REDIS zSet的键
    const LIMIT_KEY = 'limit:sliding:';
    // REDIS string的键
    const LIMIT_BAN_KEY = 'limit:ban:';

    // 过期时间
    const TTL = 1800;
    // 严重超过直接封一天
    const BAN_TTL = 86400;

    private $redis;

    public function __construct()
    {
        $this->redis = new Redis();
        $this->redis->connect('127.0.0.1', 6379);
    }

    public function checkRequest($uid)
    {
        // 是否严重超过请求限制
        $banKey = self::LIMIT_BAN_KEY . $uid;
        $isBan = $this->redis->get($banKey);
        if ($isBan) {
            return false;
        }

        // 得到微秒 粒度太粗不好测
        // startTime 与 nowTime 形成了一个窗口的时间范围
        $nowTime = microtime(true) * 1000;
        $startTime = $nowTime - self::LIMIT_UNIT * 1000;

        $zSetKey = self::LIMIT_KEY . $uid;

        // 获取时间范围内的请求数据
        $requestHistory = $this->redis->zRangeByScore($zSetKey, $startTime, $nowTime);

        $count = count($requestHistory);
        // 首先判断是否严重超过限制
        if ($count > self::LIMIT_BAN_COUNT) {
            // 直接封
            $this->redis->set($banKey, 1, self::BAN_TTL);
            return false;
        }

        // 将下面的REDIS命令打包，使一组命令具有原子性
        $this->redis->multi();
        // 添加
        $options = [];
        $value = $uid . ':' . $nowTime . rand(0, 999);
        // 多余的数据删除
        $this->redis->zRemRangeByScore($zSetKey, 0, $startTime);
        // 添加数据
        $this->redis->zAdd($zSetKey, $options, $nowTime, $value);
        // 设置过期时间
        $this->redis->expire($zSetKey, self::TTL);
        // 执行这一组命令
        $this->redis->exec();

        if ($count >= self::LIMIT_COUNT) {
            return false;
        } else {
            return true;
        }
    }


}